package edu.mcw.rgd.clinminer.web.userrole;

import com.vaadin.ui.AbstractTextField.TextChangeEventMode;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.ComboBox;
import com.vaadin.ui.Component;
import com.vaadin.ui.CustomComponent;
import com.vaadin.ui.Field;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.Layout;
import com.vaadin.ui.NativeButton;
import com.vaadin.ui.Panel;
import com.vaadin.ui.TabSheet;
import com.vaadin.ui.TabSheet.SelectedTabChangeEvent;
import com.vaadin.ui.TabSheet.SelectedTabChangeListener;
import com.vaadin.ui.TextField;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.themes.Reindeer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Scope;
import org.springframework.web.context.WebApplicationContext;
import org.vaadin.navigator.Navigator;

import com.vaadin.Application;
import com.vaadin.data.Container;
import com.vaadin.data.Property;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.util.BeanItemContainer;
import com.vaadin.event.FieldEvents.TextChangeEvent;
import com.vaadin.event.FieldEvents.TextChangeListener;

import edu.mcw.rgd.clinminer.data.ClinminerException;
import edu.mcw.rgd.clinminer.domain.Study;
import edu.mcw.rgd.clinminer.umls.AUITerm;
import edu.mcw.rgd.clinminer.umls.domain.MrConso;
import edu.mcw.rgd.clinminer.umls.type.AUI;
import edu.mcw.rgd.clinminer.umls.type.LocalCode;
import edu.mcw.rgd.clinminer.web.utils.NotificationErrorTop;
import edu.mcw.rgd.clinminer.web.utils.NotificationInfoMiddle;

@Configurable
@Scope(WebApplicationContext.SCOPE_SESSION)
public class OntologyView extends CustomComponent implements Navigator.View,
		SelectedTabChangeListener {
	private static final Logger log = Logger.getLogger(OntologyView.class);

	private VerticalLayout browserContent;

	private Panel parentsPanel;
	private Panel siblingsPanel;
	private Panel childrenPanel;

	private Navigator navigator;

	private VerticalLayout parentsLayout;

	private VerticalLayout siblingsLayout;

	private VerticalLayout childrenLayout;

	@PersistenceContext(unitName = "persistenceUnitUMLS")
	transient EntityManager entityManager;

	private Map<String, String> rootConcepts = new LinkedHashMap<String, String>() {
		private static final long serialVersionUID = 1L;

		{
			put("SNOMED CT", "A3684559");
			put("LOINC", "A6321000");
			put("NDF-RT", "A17892768");
			put("ICD-10-CM", "A20098492");
			put("ICD-9-CM", "A18090800");
			put("HCPCS", "A13475665");
			put("CPT", "A13475664");
			put("MedDRA", "A1605890");
			put("MedlinePlus", "A15660722");
			put("MeSH", "A0434168");
			put("OMIM", "A11979784");
		}
	};

	private Map<String, String> tabSources = new HashMap<String, String>() {
		private static final long serialVersionUID = 1L;

		{
			put("SNOMEDCT", "SNOMED CT");
			put("LNC", "LOINC");
			put("NDFRT", "NDF-RT");
			put("ICD10CM", "ICD-10-CM");
			put("ICD9CM", "ICD-9-CM");
			put("MTHHH", "HCPCS");
			put("MTHCH", "CPT");
			put("MDR", "MedDRA");
			put("MEDLINEPLUS", "MedlinePlus");
			put("MSH", "MeSH");
			put("OMIM", "OMIM");
		}
	};

	private TabSheet tabsheet;

	private BeanItemContainer<Study> containerForStudies;

	private ComboBox studyCombo;

	private String currentView;

	private Layout searchBoxPlaceholder;

	private String currentSource;

	public OntologyView() {
		tabsheet = new TabSheet();
		tabsheet.setData(true); // navigates to root on tab change
		tabsheet.setSizeFull();
		tabsheet.addStyleName(Reindeer.TABSHEET_BORDERLESS);
		tabsheet.addListener(this);
		setCompositionRoot(tabsheet);

		buildBrowserContent();

		Map<String, Panel> tabs = new HashMap<String, Panel>();
		for (String source : rootConcepts.keySet()) {
			Panel p = new Panel();
			p.setStyleName(Reindeer.PANEL_LIGHT);
			p.setSizeFull();
			tabs.put(source, p);
			tabsheet.addTab(p, source);
		}
	}

	@Override
	public void init(Navigator navigator, Application application) {
		this.navigator = navigator;
	}

	protected void navigateToFragment(String fragment) {
		if (navigator != null) {
			String uri = navigator.getUri(getClass());
			// remove fragment
			uri = uri.replaceAll("/.*", "");
			if (fragment == null) {
				fragment = "";
			} else {
				fragment = "/" + fragment;
			}
			navigator.navigateTo(uri + fragment);
		}
	}

	@Override
	public void navigateTo(String requestedDataId) {
		String action;
		String query;

		// set default
		if (requestedDataId == null) {
			action = "term";
			query = "A3684559"; // snomed root
		} else if (!requestedDataId.contains("/")) {
			// warn of unrecognised uri
			getApplication().getMainWindow().showNotification(
					new NotificationErrorTop("Navigation Error", "Uri <"
							+ requestedDataId + "> not recognized"));
			action = "term";
			query = "A3684559"; // snomed root
		} else {
			// process path normally
			action = requestedDataId.split("/")[0];
			try {
				query = requestedDataId.split("/")[1];
			}
			// empty query after /
			catch (ArrayIndexOutOfBoundsException e) {
				query = "";
			}
		}

		if (action.equals("term")) {
			try {
				createView(query);
			} catch (ClinminerException e) {
				getApplication().getMainWindow().showNotification(
						new NotificationErrorTop("Navigation Error", e
								.getMessage()));
			}
		} else if (action.equals("search")) {
			if (currentSource == null) {
				currentSource = "SNOMEDCT";
				configureSearchField();
				// this is confusing and interrupts typing
				// unless done only on first entry
				((TextField) searchBoxPlaceholder.getComponentIterator().next())
						.setValue(query);
			}
			try {
				createSearchResultView(query);
			} catch (ClinminerException e) {
				getApplication().getMainWindow()
						.showNotification(
								new NotificationErrorTop("Search Error", e
										.getMessage()));
			}
		} else {
			getApplication().getMainWindow().showNotification(
					new NotificationErrorTop("Unknown error", "Notify admin"));
		}

	}

	private void createView(String accession) throws ClinminerException {
		AUITerm mainTerm = new AUITerm(new AUI(accession));

		currentView = accession;
		currentSource = mainTerm.getSourceAbbreviation();

		configureSearchField();

		// align tab with term source when comning from a bookmarked uri
		String selectedSource = tabsheet.getTab(tabsheet.getSelectedTab())
				.getCaption();
		String termSource = tabSources.get(currentSource);
		// update selected tab
		if (termSource != null) {
			if (!selectedSource.equals(termSource)) {
				for (int i = 0; i < tabsheet.getComponentCount(); i++) {
					if (termSource.equals(tabsheet.getTab(i).getCaption())) {
						// prevent tab change listener from firing and
						// redirecting
						// to root
						tabsheet.setData(false);
						tabsheet.setSelectedTab(i);
						break;
					}
				}
			}
		} else {
			getApplication().getMainWindow().showNotification(
					new NotificationErrorTop("Navigation Error",
							"Unsupported UMLS source terminology"));
		}

		// add concepts
		Integer topPadding = 0;
		Boolean keepPadding = true;

		Collection<AUITerm> siblings = mainTerm.getSiblings(getStudy());
		Collection<AUITerm> parents = mainTerm.getParents(getStudy());
		// hidden terms are not connected within the indexed tree
		if (parents.isEmpty()) {
			parents = mainTerm.getParents(null);
		}
		Collection<AUITerm> children = mainTerm.getChildren(getStudy());

		parentsPanel.setEnabled(!parents.isEmpty());
		childrenPanel.setEnabled(!children.isEmpty());

		// siblings
		siblingsPanel.setCaption("Active Term + Siblings");
		siblingsPanel.addStyleName("c-ontology-panel");
		siblingsPanel.removeStyleName("c-search-panel");
		parentsPanel.setCaption("Parent Terms");
		parentsPanel.addStyleName("c-ontology-panel");
		childrenPanel.setCaption("Child Terms");
		childrenPanel.addStyleName("c-ontology-panel");

		siblingsLayout.removeAllComponents();
		siblingsLayout.addComponent(siblingsPanel);
		siblingsPanel.removeAllComponents();
		for (AUITerm term : siblings) {
			if (term.getAccession().equals(accession)) {
				siblingsPanel.addComponent(createActiveTerm(term));
				keepPadding = false;
			} else {
				siblingsPanel.addComponent(createTerm(term));
			}
			if (keepPadding) {
				topPadding++;
			}
		}

		// parents
		parentsLayout.removeAllComponents();
		parentsPanel.removeAllComponents();
		parentsLayout.addComponent(createPadding(topPadding));
		parentsLayout.addComponent(parentsPanel);
		for (AUITerm parent : parents) {
			parentsPanel.addComponent(createTerm(parent));
		}

		// children
		childrenLayout.removeAllComponents();
		childrenLayout.addComponent(createPadding(topPadding));
		childrenPanel.removeAllComponents();
		childrenLayout.addComponent(childrenPanel);
		for (AUITerm child : children) {
			childrenPanel.addComponent(createTerm(child));
		}

	}

	private void configureSearchField() {
		final TextField searchBox = new TextField();
		searchBox.setInputPrompt("Separate keywords with spaces");
		searchBox.setTextChangeEventMode(TextChangeEventMode.LAZY);
		searchBox.setTextChangeTimeout(500);

		searchBox.addListener(new TextChangeListener() {
			@Override
			public void textChange(TextChangeEvent event) {
				if (event.getText().isEmpty()) {
					// clear search
					try {
						createView(currentView);
					} catch (ClinminerException e) {
						e.printStackTrace();
					}
				}
				if (event.getText().length() < 3) {
					return;
				}
				// createSearchResultView(event.getText(), currentSource);
				navigateToFragment("search/" + event.getText());
			}

		});

		searchBox.setWidth(300, UNITS_PIXELS);
		searchBoxPlaceholder.removeAllComponents();
		searchBoxPlaceholder.addComponent(searchBox);
	}

	private void createSearchResultView(String query) throws ClinminerException {

		List<String> keywords = Arrays.asList(query.split(" "));

		// prepare query string
		List<String> validTokens = new ArrayList<String>();
		for (String token : query.split(" ")) {
			// remove one letter tokens (overloads oracle context index on
			// wildcard expansion)
			if (token.length() > 1) {
				// remove nonalphanumerics
				validTokens.add(token.replaceAll("[^a-zA-Z0-9\\s\\.]", ""));
			}
		}
		String queryString = StringUtils.join(validTokens, " ");

		// add concepts
		Collection<AUITerm> siblings = new ArrayList<AUITerm>();
		long startTime = System.nanoTime();
		Collection<String> result = new MrConso().findSearchSuggestionCodes(
				queryString, currentSource, 50);
		long endTime = System.nanoTime();
		for (String localCode : result) {
			siblings.add(new AUITerm(new LocalCode(localCode)));
		}

		siblingsLayout.removeAllComponents();
		siblingsPanel.addStyleName("c-ontology-panel");
		siblingsPanel.addStyleName("c-search-panel");

		// header with results count
		// count only if the original query was less than 500ms
		// for improved responsiveness
		Number totalCount = null;
		if ((endTime - startTime) < 500000000) {
			totalCount = MrConso.countSearchSuggestions(queryString,
					currentSource);
		}

		String summary = "";
		String duration = String.format("%.3f",
				(endTime - startTime) / 1000000000.0);
		if (siblings.size() > 0) {
			summary = "Found <strong>" + siblings.size() + "</strong> results ";
			if (totalCount != null) {
				summary += "out of " + totalCount + " ";
			}
			summary += "for <strong>" + query + "</strong> in " + duration
					+ " s";
		} else {
			summary = "No results found for <strong>" + queryString
					+ "</strong>";
		}
		siblingsPanel.setCaption(summary);

		// siblings
		siblingsLayout.addComponent(siblingsPanel);
		siblingsPanel.removeAllComponents();

		for (AUITerm term : siblings) {
			siblingsPanel.addComponent(createTerm(term));
		}

		parentsPanel.setEnabled(false);
		childrenPanel.setEnabled(false);
		parentsLayout.removeAllComponents();
		childrenLayout.removeAllComponents();
	}

	private Component createActiveTerm(final AUITerm term)
			throws ClinminerException {
		VerticalLayout lMain = new VerticalLayout();
		VerticalLayout lDetails = new VerticalLayout();
		lDetails.addStyleName("c-ontology-details");

		Component activeTerm = createTerm(term);
		activeTerm.addStyleName("c-ontology-item-selected");

		addDetailLabel(lDetails, "Definition", term.getDefinitions());
		addDetailLabel(lDetails, "Synonym", term.getSynonyms());
		addDetailLabel(lDetails, "Local Code", new ArrayList<String>() {
			{
				add(term.getLocalCode());
			}
		});
		addDetailLabel(lDetails, "UMLS concept", new ArrayList<String>() {
			{
				add(term.getCui());
			}
		});

		lMain.addComponent(activeTerm);
		lMain.addComponent(lDetails);
		return lMain;
	}

	private void addDetailLabel(Layout layout, String caption,
			Collection<String> col) {
		if (col.isEmpty()) {
			return;
		}
		Label label = new Label("<strong>" + caption
				+ (col.size() == 1 ? "" : "s") + ": </strong>"
				+ StringUtils.join(col, ", "), Label.CONTENT_XHTML);
		label.addStyleName(Reindeer.LABEL_SMALL);
		label.setWidth(90, UNITS_PERCENTAGE);
		layout.addComponent(label);
	}

	private Component createPadding(Integer topPadding) {
		Label spacer = new Label("");
		spacer.setHeight(27 * topPadding, UNITS_PIXELS);
		return spacer;
	}

	private Study getStudy() {
		return (Study) studyCombo.getValue();
	}

	private Component createTerm(final AUITerm term) throws ClinminerException {
		Button b = new NativeButton();
		String caption = term.getLabel();
		Integer count = term.getAnnotationCount(getStudy());
		if (count > 0) {
			caption += " (" + count + ")";
			b.addStyleName("c-ontology-item-annotated");
		}
		if (term.hasChildren()) {
			caption += " +";
		}

		b.setCaption(caption);
		b.addStyleName("c-ontology-item");
		b.addListener(new ClickListener() {
			private static final long serialVersionUID = 1L;

			@Override
			public void buttonClick(ClickEvent event) {
				navigateToFragment("term/" + term.getAccession());
			}

		});

		return b;
	}

	@Override
	public String getWarningForNavigatingFrom() {
		return null;
	}

	@Override
	public void selectedTabChange(SelectedTabChangeEvent event) {
		TabSheet tabsheet = (TabSheet) event.getSource();
		// set up loading message

		for (int i = 0; i < tabsheet.getComponentCount(); i++) {
			Panel p = ((Panel) tabsheet.getTab(i).getComponent());
			p.setContent(null);
		}
		final Panel selectedTab = (Panel) tabsheet.getSelectedTab();
		selectedTab.setContent(browserContent);

		String selectedCaption = tabsheet.getTab(selectedTab).getCaption();

		// skip root navigation if came from bookmark
		if (tabsheet.getData() instanceof Boolean
				&& (Boolean) tabsheet.getData() == true) {
			navigateToFragment("term/" + rootConcepts.get(selectedCaption));
		}
		tabsheet.setData(true);
	}

	private void buildBrowserContent() {
		// set up
		browserContent = new VerticalLayout();
		browserContent.setSizeFull();
		browserContent.setMargin(true);

		Label spacer1 = new Label("");
		spacer1.setHeight(3, UNITS_EM);
		Label spacer2 = new Label("");
		spacer2.setHeight(3, UNITS_EM);
		Label spacer3 = new Label("");
		spacer3.setHeight(3, UNITS_EM);
		spacer3.setWidth(3, UNITS_EM);

		Layout searchLayout = buildSearchLayout();
		Panel searchPanel = new Panel();
		searchPanel.setSizeUndefined();
		searchPanel.setCaption("Search");
		searchPanel.addComponent(searchLayout);
		searchPanel.setStyleName(Reindeer.PANEL_LIGHT);

		Field studyCombo = buildStudyCombo();
		Panel studyPanel = new Panel();
		studyPanel.setSizeUndefined();
		studyPanel.setCaption("Data-driven perspective");
		studyPanel.addComponent(studyCombo);
		studyPanel.setStyleName(Reindeer.PANEL_LIGHT);

		HorizontalLayout horLayout = new HorizontalLayout();
		horLayout.setSizeUndefined();
		horLayout.addComponent(searchPanel);
		horLayout.addComponent(spacer3);
		horLayout.addComponent(studyPanel);

		browserContent.addComponent(spacer1);
		browserContent.addComponent(horLayout);
		browserContent.addComponent(spacer2);

		// browserContent.addComponent(studyPanel);
		// browserContent.addComponent(spacer3);

		// Layout buttonLayout = buildButtonLayout();
		// browserContent.addComponent(buttonLayout);

		Layout browserPanel = buildBrowserLayout();
		browserContent.addComponent(browserPanel);

		// snomedContent.setExpandRatio(browserPanel, 1.0f);

		// browserContent
		// .setComponentAlignment(searchLayout, Alignment.TOP_CENTER);
		// browserContent.setComponentAlignment(buttonLayout,
		// Alignment.MIDDLE_CENTER);
		// browserContent.setComponentAlignment(studyPanel,
		// Alignment.MIDDLE_CENTER);
		browserContent.setComponentAlignment(browserPanel,
				Alignment.BOTTOM_CENTER);
		browserContent.setComponentAlignment(horLayout, Alignment.TOP_CENTER);

	}

	private Field buildStudyCombo() {
		studyCombo = new ComboBox();
		studyCombo.setWidth(300, UNITS_PIXELS);
		studyCombo.setInputPrompt("Select study...");

		// configure & load content
		studyCombo.setImmediate(true);
		studyCombo.setItemCaptionPropertyId("name");
		studyCombo.setContainerDataSource(getContainerForStudies());
		studyCombo.setNewItemsAllowed(false);

		studyCombo.addListener(new Property.ValueChangeListener() {

			@Override
			public void valueChange(ValueChangeEvent event) {
				// reload current page
				navigateTo("term/" + currentView);
				getApplication().getMainWindow().showNotification(
						new NotificationInfoMiddle("Updated ontology tree...",
								""));
			}

		});

		return studyCombo;
	}

	private Container getContainerForStudies() {
		if (containerForStudies == null) {
			BeanItemContainer<Study> container = new BeanItemContainer<Study>(
					Study.class, Study.findAllStudies());
			containerForStudies = container;
		}
		return containerForStudies;
	}

	private Layout buildSearchLayout() {
		Label spacer2 = new Label("");
		spacer2.setHeight(3, UNITS_EM);

		// searchBox.focus();
		searchBoxPlaceholder = new HorizontalLayout();
		// searchBox.setInputPrompt("Search...");
		// buildSearchBoxCombo(facetedLabel, popup);

		Button searchButton = new Button("Search");
		HorizontalLayout searchLayout = new HorizontalLayout();

		// faceted navigation goes to the right

		searchLayout.addComponent(searchBoxPlaceholder);
		searchLayout.setSpacing(true);
		searchLayout.addComponent(searchButton);

		searchLayout.setSizeUndefined();
		return searchLayout;
	}

	// private Layout buildButtonLayout() {
	// HorizontalLayout layout = new HorizontalLayout();
	// final String bDataDrivenOn = "Show All";
	// final String bDataDrivenOff = "Hide Empty Branches";
	//
	// final Button bData = new Button(bDataDrivenOn);
	//
	// bData.setStyleName(Reindeer.BUTTON_LINK);
	// bData.addListener(new ClickListener() {
	//
	// @Override
	// public void buttonClick(ClickEvent event) {
	// // toggle
	// isDataDriven = !isDataDriven;
	//
	// // updateTableContainer();
	//
	// if (isDataDriven) {
	// event.getButton().setCaption(bDataDrivenOn);
	// bData.setEnabled(true);
	// } else {
	// event.getButton().setCaption(bDataDrivenOff);
	// bData.setEnabled(false);
	// }
	// }
	// });
	//
	// layout.addComponent(bData);
	// layout.setSpacing(true);
	// layout.setMargin(true);
	// layout.setComponentAlignment(bData, Alignment.MIDDLE_CENTER);
	//
	// return layout;
	// }

	private Layout buildBrowserLayout() {
		HorizontalLayout mainLayout = new HorizontalLayout();
		mainLayout.setSizeFull();

		parentsPanel = new Panel();
		siblingsPanel = new Panel();
		childrenPanel = new Panel();

		parentsPanel.setSizeFull();
		siblingsPanel.setSizeFull();
		childrenPanel.setSizeFull();

		parentsLayout = new VerticalLayout();
		siblingsLayout = new VerticalLayout();
		childrenLayout = new VerticalLayout();
		parentsLayout.setSizeFull();
		siblingsLayout.setSizeFull();
		childrenLayout.setSizeFull();

		mainLayout.addComponent(parentsLayout);
		mainLayout.addComponent(siblingsLayout);
		mainLayout.addComponent(childrenLayout);
		mainLayout.setExpandRatio(parentsLayout, 1.0f);
		mainLayout.setExpandRatio(siblingsLayout, 1.0f);
		mainLayout.setExpandRatio(childrenLayout, 1.0f);

		return mainLayout;
	}

}
